Skip to content

Add Subtitles row to MovieDetails component. Fix incorrect initially selected subtitle stream.#728

Open
dongrote wants to merge 7 commits intojellyfin:masterfrom
dongrote:add-subtitles-row-to-movie-details
Open

Add Subtitles row to MovieDetails component. Fix incorrect initially selected subtitle stream.#728
dongrote wants to merge 7 commits intojellyfin:masterfrom
dongrote:add-subtitles-row-to-movie-details

Conversation

@dongrote
Copy link

@dongrote dongrote commented Feb 9, 2026

Changes

Encapsulates the selected video, audio, and subtitle streams into a MovieDetailsStreamSummary component. Fix incorrect initially selected subtitle stream in MovieOptions.

Adds a Subtitles row to the MovieDetails component to indicate to the user which subtitle stream is selected, along with the video and audio streams.

Fixes the initial selected subtitles stream in MovieOptions. The initial selected subtitles stream comes to us from the Jellyfin server and if it is not present there, then we default to None.

Issues

Fixes #526 and #626

jellyfin-ss-outline

Encapsulates the selected video, audio, subtitle streams into a
MovieDetailsStreamSummary component.

Adds a Subtitles row to the MovieDetails component to indicate to the
user which subtitle stream is selected, along with the video and audio
streams.

Fixes the initial selected subtitles stream in MovieOptions. The
initial selected subtitles stream comes to us from the Jellyfin server
and if it is not present there, then we default to None.
@dongrote dongrote requested a review from a team as a code owner February 9, 2026 13:39
@dongrote
Copy link
Author

dongrote commented Feb 9, 2026

I understand this is a rather large charge consisting of multiple things, but I was on a roll and wanted to get some feedback on all of it.

The big pieces:

  • Refactor MovieDetails selected streams.
  • Fix initially selected subtitles stream.
  • When multiple MediaSources are present, the Video stream name is also included in the select Video stream text (just remembered I did that when I looked at the screenshot).

Let me know if you'd prefer that I break this up into a series of smaller pull requests, or if there are any of those pieces that we simply do not want.

@1hitsong
Copy link
Member

Very cool! I love the display component.

In my functionality testing, found an unhappy path.

I have a video that has both embedded subtitles and an external subtitle file. If I select the embedded subtitles prior to playback, it does change the displayed subtitle track, but on playback, the external track is played and shows as selected in the subtitle popup.

The external track is the default according to the server, but I manually selected the embedded track. So my selection should override the default selection.

selection.mp4

mediaStreams = itemData.mediaStreams

for i = 0 to mediaStreams.Count() - 1
if mediaStreams[i].Type = "Audio"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add AUDIO to the MediaStreamType ENUM and use it here.

if isStringEqual(chainLookup(mediaStreams[i], "Type"), MediaStreamType.AUDIO)

selectedSubtitleStreamTitle = "None"
for i = 0 to itemData.mediaStreams.Count() - 1
stream = itemData.mediaStreams[i]
if stream.Type = "Subtitle"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use enum, isStringEqual, and chain lookup.

Comment on lines +738 to +768
function findStreamSummaryNode() as dynamic
node = m.top.findNode("streamSummary")
return node
end function

sub setRotationWarning(rotationWarning as string)
findStreamSummaryNode().rotationWarning = rotationWarning
end sub

sub setSelectedVideoStreamTitle(title as string)
findStreamSummaryNode().selectedVideoStreamTitle = title
end sub

sub setVideoStreamCount(count as integer)
findStreamSummaryNode().videoStreamCount = count
end sub

sub setSelectedAudioStreamTitle(title as string)
findStreamSummaryNode().selectedAudioStreamTitle = title
end sub

sub setAudioStreamCount(count as integer)
findStreamSummaryNode().audioStreamCount = count
end sub

sub setSelectedSubtitleStreamTitle(title as string)
findStreamSummaryNode().selectedSubtitleStreamTitle = title
end sub

sub setSubtitleStreamCount(count as integer)
findStreamSummaryNode().subtitleStreamCount = count
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these are assuming findStreamSummaryNode() returns a valid node. Defensive coding requires us to confirm it is valid prior to using it, otherwise it'll crash out.

Additionally, you're finding the node over and over again inside of code that will most likely be run multiple times. I'm cool running findnode on demand for things that wait for an event, like a keypress, but since we will use this in most scenarios, put the find in the init as an m. variable and just use it in all these functions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there isn't any checking for isValid() when we use findNode() in the init() subroutine. Is it guaranteed we'll find it while in the scope of init()?

When I assign the streamSummary node to m.streamSummary, does that remove the need to check for validity in all of these setters?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, you'll still need to check it's valid before using it, it simply saves the time findnode() would take to traverse the XML to find the node as the node reference is saved/remembered if it's found.

@@ -0,0 +1,12 @@
sub onChangeTitle()
streamTitle = m.top.title
node = m.top.findNode("title")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumes the node is found. Before accessing the text param, we must perform an isValid() check.

count = m.top.count
node = m.top.findNode("count")
additionalCount = count - 1
node.text = additionalCount > 0 ? "+" + stri(additionalCount).trim() : ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumes the node is found. Before accessing the text param, we must perform an isValid() check.

@dongrote
Copy link
Author

@1hitsong thank you for all of your fantastic feedback and suggestions!

@dongrote
Copy link
Author

In my functionality testing, found an unhappy path.

I have a video that has both embedded subtitles and an external subtitle file. If I select the embedded subtitles prior to playback, it does change the displayed subtitle track, but on playback, the external track is played and shows as selected in the subtitle popup.

The external track is the default according to the server, but I manually selected the embedded track. So my selection should override the default selection.

Thank you so much for exercising this edge case! I don't have any media files with external subtitle tracks. If I have difficulty setting up the same scenario, I'll reach out for help in reproducing this on my end.

@1hitsong
Copy link
Member

You're very welcome.

Just so you know some of the WHY we use the helper functions.

isStringEqual()

  • confirms both params are strings
  • confirms both params are valid
  • converts both params to the same casing
  • confirms both params are equal

chainLookup()

  • confirms each property step is valid before continuing to next nested property
  • performs case insensitive lookups - because parent.child is not the same as parent.Child
  • Returns invalid at first invalid property or returns final property value

chainLookupReturn()

  • same as chainLookup, but instead of returning invalid at first invalid property, returns the desired default value. Allowing you to do cool stuff like if chainLookupReturn(parent, "child.boolValue", false) and have it be safe.

@dongrote
Copy link
Author

Just so you know some of the WHY we use the helper functions.

Thank you for all of these explanations!

Co-authored-by: 1hitsong <3330318+1hitsong@users.noreply.github.com>
@dongrote
Copy link
Author

chainLookup()

  • confirms each property step is valid before continuing to next nested property
  • performs case insensitive lookups - because parent.child is not the same as parent.Child
  • Returns invalid at first invalid property or returns final property value

Should I just use chainLookup() every where that I access properties? Are there any scenarios where I explicitly should not use chainLookup()?

Keeping track of what is case insensitive and what isn't is wild! 😆 😢

@1hitsong
Copy link
Member

Are there any scenarios where I explicitly should not use chainLookup()?

If you're using the same value over and over, use chainlookup once and save the value to a variable.

Or, if you're using an object in the chain over and over, save it to a variable then use it as the root element in chainLookup().

The reason is so you're not looping through the same properties checking for valid over and over and over again. Once we know it's valid, it's just wasted cycles checking the same thing repeatedly.

Keeping track of what is case insensitive and what isn't is wild! 😆 😢

🤘🏻 Exactly!

- utilize isValid, isValidAndNotEmpty, isStringEqual, chainLookup, etc.
- add MediaStreamType enum values
- find XML nodes in `init()` subroutines
@dongrote
Copy link
Author

I think I addressed all of the pull request comments. I still need to investigate the external subtitles bug.

@1hitsong
Copy link
Member

I still need to investigate the external subtitles bug.

Any luck tracking it down?

@dongrote
Copy link
Author

Any luck tracking it down?

Sorry, life got busy! I expect I'll sadly be busy this upcoming weekend, too. I did add external subtitles to a show on my system. I wasn't able to get them to render during playback (not sure why). But I don't expect rendering them is a concern for this PR. I did notice in your video that it looked like the Subtitle selection quickly switches back to external in between dismissing the MovieOptions and starting playback. If I recall, that was the only lead I had to work with when I was last able to sit down and focus on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Subtitles are burned in upon transcoding even when not enabled

2 participants